﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Lync.Model;
using Microsoft.Win32;
using SpotifyAPI;
using System.IO;
using System.ServiceModel;

namespace SpotifyLync
{
    public class App
    {
        static MenuItem Updates;
        static MenuItem AllowControl;
        static NotifyIcon n;
        static Timer t;
        static ITrackInfo currentStatus;
        static LyncClient client;
        static ISpotifyRemoteLocalInterop svc;
        static bool resumeOnUnlock = false;
        static bool isRemote = false;
        static NLog.Logger l;
        static ServiceHost host;

        /// <summary>
        /// Entry point for the application.
        /// </summary>
        public static void Main()
        {
            l = NLog.LogManager.GetLogger("SpotifyLync");
            InitializeServiceHost();
            InitializeTrayIcon();
            InitializeLync();
            InitializeSpotify();
            InitializeSystemEvents();
            DoUpdate();
            InitializeTimer();
            
            Application.EnableVisualStyles();

            // Start a new message pump so the application doesn't exit on us.
            Application.Run();
        }

      
        /// <summary>
        /// Initializes the timer that ticks every 10 seconds to poll Spotify.
        /// </summary>
        public static void InitializeTimer()
        {
            t = new Timer();
            t.Interval = 10 * 1000;
            t.Tick += (_, __) => DoUpdate();
            t.Enabled = true;
        }

        /// <summary>
        /// Creates a new Lync client.
        /// </summary>
        public static void InitializeLync()
        {
            client = LyncClient.GetClient();
        }


        /// <summary>
        /// Initializes the remote service host.
        /// </summary>
        private static void InitializeServiceHost()
        {
            // Set up the service
            host = new ServiceHost(typeof(Service.SpotifyRemoteService));
        }


        /// <summary>
        /// Initializes the Spotify web helper.
        /// </summary>
        public static void InitializeSpotify()
        {
            l.Debug("Attempting to initialize Spotify");
            try
            {
                // If we have a known remote Ip, use it.
                string remoteIp = Properties.Settings.Default.RemoteSpotifyPC;
                if (string.IsNullOrEmpty(remoteIp))
                {
                    l.Debug("Using Local Instance");
                    svc = new Service.SpotifyRemoteService();
                    isRemote = false;
                }
                else
                {
                    l.Debug("Attempting remote instance on " + remoteIp);
                    
                    // Set a uri for our endpoint address                    
                    var address = new EndpointAddress(@"net.tcp://" + remoteIp + ":29890/SpotifyRemoteService");
                    svc = new SpotifyRemoteServiceClient("TcpService", address);
                    
                    isRemote = true;
                }
                l.Debug("Spotify Initialized" + svc.GetType().ToString());
            }
            catch (Exception ex)
            {
                l.ErrorException("Failed to initialize Spotify", ex);
                svc = null;
            }
        }

        public static void InitializeSystemEvents()
        {
            SystemEvents.SessionSwitch += (_, e) =>
            {
                bool setAway = Properties.Settings.Default.SetAwayOnLock;
                bool setPause = Properties.Settings.Default.PauseSpotifyOnLock;
                switch (e.Reason)
                {
                    case SessionSwitchReason.SessionLock:
                        if (setAway)
                        {
                            SetLyncPresence(ContactAvailability.Away);
                        }

                        if ((setPause) &&(svc.IsPlaying()))
                        { 
                            svc.PlayPause();

                            // let the app know to resume when unlocked.
                            resumeOnUnlock = true;
                        }
                        break;
                    case SessionSwitchReason.SessionUnlock:
                        if (setAway)
                        {
                            SetLyncPresence(ContactAvailability.None);
                        }

                        if (setPause && resumeOnUnlock && !svc.IsPlaying())
                        {
                            svc.PlayPause();
                        }

                        // Whether or not it was playing, it shouldn't resume 
                        // now unless it is set again by process.
                        resumeOnUnlock = false;
                        break;
                }
            };
        }

        /// <summary>
        /// Initializes the tray icon and associated context menu.
        /// </summary>
        public static void InitializeTrayIcon()
        {
            n = new NotifyIcon()
            {
                Icon = new Icon("Resources/App.ico"),
                Text = "SpotifyLync " + GetVersion()
            };
            
            Updates = new MenuItem("Update", (_, __) => Update_Click());
            Updates.Checked = Properties.Settings.Default.DisplayCurrentSong;

            AllowControl = new MenuItem("Allow Remote Control", (_,___) => AllowControl_Click());
            AllowControl.Checked = Properties.Settings.Default.AllowExternalControl;

            // Initialize the service if it was running last time.
            ToggleRemoteService(AllowControl.Checked);

            n.ContextMenu = new ContextMenu(new MenuItem[] {
                Updates,
                AllowControl,
                new MenuItem("-"),
                new MenuItem("Previous Song", (_,__) => svc.PlayPrevious()),
                new MenuItem("Play/Pause", (_,  __) => svc.PlayPause()),
                new MenuItem("Next Song", (_, __) => svc.PlayNext()),
                new MenuItem("-"),
                new MenuItem("Settings", (__, _) => Settings()),
                new MenuItem("-"),
                new MenuItem("About", (_,__) => About()),
                new MenuItem("Exit", (_, __) => Exit())
            });

            n.Visible = true;

            ShowBalloonTip("Welcome to SpotifyLync!");
            l.Debug("Initialized Tray Icon");
        }

        public static void Settings()
        {
            Forms.Settings s = new Forms.Settings();
            s.ShowDialog();
        }

        /// <summary>
        /// Updates your own Lync status with a new message.
        /// </summary>
        /// <param name="newStatus">The new message, or <c>null</c> to reset the status.</param>
        public static void UpdateLyncStatus(string newStatus)
        {
            if (Properties.Settings.Default.DisplayCurrentSong)
            {
                l.Debug("Attempting to update Lync Status");
                if (client != null && client.State == ClientState.SignedIn)
                {
                    var pkg = new Dictionary<PublishableContactInformationType, object>();
                    pkg.Add(PublishableContactInformationType.PersonalNote, newStatus);
                    client.Self.BeginPublishContactInformation(pkg, null, null);
                }
                l.Debug("Updated Lync Status");
            }
        }

        /// <summary>
        /// Updates your own Lync presence with a new state.
        /// </summary>
        /// <param name="newState">The new state to publish.</param>
        public static void SetLyncPresence(ContactAvailability newState)
        {
            if (Properties.Settings.Default.SetAwayOnLock)
            {
                if (client != null && client.State == ClientState.SignedIn)
                {
                    var pkg = new Dictionary<PublishableContactInformationType, object>();
                    pkg.Add(PublishableContactInformationType.Availability, newState);
                    client.Self.BeginPublishContactInformation(pkg, null, null);
                }
            }
        }

        /// <summary>
        /// Retrieves the formatted current song information.
        /// </summary>
        /// <returns>The current song, formatted.</returns>
        public static string GetCurrentSongFormatted()
        {
            try
            {
                if (currentStatus != null)
                {
                    return "♪ " + Filter(currentStatus.Artist) + " – " + Filter(currentStatus.Title) + " ♫";
                }
                else
                {
                    return null;
                }
            }
            catch (Exception ex)
            {
                l.ErrorException("Failed getting current song (formatted)", ex);
                return null;
            }
        }

        /// <summary>
        /// Polls the Spotify local API for new song information.
        /// </summary>
        /// <returns></returns>
        public static bool UpdateSpotifyStatus()
        {
            try
            {
                l.Debug("Attempting to update status");
                string newStatus = svc.GetTrackInfo();
                ITrackInfo value = null;

                if (!string.IsNullOrEmpty(newStatus))
                {
                    value = Newtonsoft.Json.JsonConvert.DeserializeObject<TrackInfo>(svc.GetTrackInfo());
                }

                if (value != null)
                {
                    currentStatus = value;
                    return true;
                }

                return false;
            }
            catch (Exception ex)
            {
                l.ErrorException("Failed to update spotify status", ex);
                return false;
            }
        }

        /// <summary>
        /// Performs an update by polling the local Spotify API for song information and updating Lync accordingly.
        /// </summary>
        public static void DoUpdate()
        {
            l.Debug("Updating Status -- DoUpdate()");
            if (UpdateSpotifyStatus())
            {
                if (svc.IsPlaying() && currentStatus != null)
                {
                    UpdateLyncStatus(GetCurrentSongFormatted());
                    var status = "SpotifyLync " + GetVersion() + "\r\n" + currentStatus.Artist + " - " + currentStatus.Title;
                    if (status.Length >= 64)
                    {
                        status = status.Substring(0, 60) + "...";
                    }
                    n.Text = status;
                }
                else
                {
                    UpdateLyncStatus(null);
                    n.Text = "SpotifyLync " + GetVersion();
                }
            }
        }

        /// <summary>
        /// Handles the check toggle in the tray context menu for turning updates on or off.
        /// </summary>
        public static void Update_Click()
        {
            Updates.Checked = !Updates.Checked;
            t.Enabled = Updates.Checked;
            if (!Updates.Checked)
            {
                UpdateLyncStatus(null);
                ShowBalloonTip("SpotifyLync is no longer updating your Lync status.");
            }
            else
            {
                UpdateLyncStatus(GetCurrentSongFormatted());
                ShowBalloonTip("SpotifyLync is now updating your Lync status automatically.");

                // Authorizing updates, implies authorizing Lync.
                Properties.Settings.Default.EnableLync = true;
            }

            Properties.Settings.Default.DisplayCurrentSong = Updates.Checked;
            Properties.Settings.Default.Save();
        }


        /// <summary>
        /// Handles the check toggle in the tray context menu for turning remote services on or off.
        /// </summary>
        /// <returns></returns>
        private static void AllowControl_Click()
        {
            AllowControl.Checked = !AllowControl.Checked;
            
            // Reinitialize since we changed something...
            InitializeSpotify();
            ToggleRemoteService(AllowControl.Checked);

            if (!AllowControl.Checked)
            {
                ShowBalloonTip("SpotifyLync is no longer hosting a remote access endpoint service.");
            }
            else
            {
                ShowBalloonTip("SpotifyLync is now hosting a remote access endpoint service.");
            }

            Properties.Settings.Default.AllowExternalControl = AllowControl.Checked;
            Properties.Settings.Default.Save();
        }

        /// <summary>
        /// Toggles the remote service host, determining whether or not to allow remote control.
        /// </summary>
        /// <param name="enable"></param>
        private static void ToggleRemoteService(bool enable)
        {
            l.Debug("Attempting to Toggle Remote Services - enable: " + enable.ToString());
            try
            {
                if (enable)
                {
                    if ((host.State != CommunicationState.Opened) &&
                        (host.State != CommunicationState.Opening))
                    {
                        // Reinitalize
                        InitializeServiceHost();
                        host.Open();
                        l.Debug("Opened Host Successfully");
                    }
                }
                else
                {
                    host.Close();
                    l.Debug("Closed Host Successfully");
                }
            }
            catch (Exception ex)
            {
                l.ErrorException("Failed to Toggle Remote Services - - enable: " + enable.ToString(), ex);
                if (host.State == CommunicationState.Faulted)
                {
                    host.Abort();
                    l.Error("Aborted host after exception");
                }
                else
                {
                    host.Close();
                    l.Error("Closed host after exception");
                }

            }
        }

        /// <summary>
        /// Shows a balloon tip with the specified text.
        /// </summary>
        /// <param name="text"></param>
        public static void ShowBalloonTip(string text)
        {
            n.BalloonTipIcon = ToolTipIcon.Info;
            n.BalloonTipTitle = "SpotifyLync";
            n.BalloonTipText = text;
            n.ShowBalloonTip(6000);
        }

        /// <summary>
        /// Retrieves a formatted version string of the current assembly.
        /// </summary>
        /// <returns>The formatted version string.</returns>
        public static string GetVersion()
        {
            return Assembly.GetExecutingAssembly().GetName().Version.ToString();
        }

        /// <summary>
        /// Shows the about message.
        /// </summary>
        public static void About()
        {
            MessageBox.Show("SpotifyLync v" + GetVersion() + "\r\n\r\nhttp://spotifylync.codeplex.com/", "SpotifyLync", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        
        /// <summary>
        /// Exits the program with a success code (0).
        /// </summary>
        public static void Exit()
        {
            Environment.Exit(0);
        }

        /// <summary>
        /// Filters out bad words so they don't show up in the Lync status.
        /// 
        /// TODO: Make this somehow work with words like "assassin".
        /// </summary>
        /// <param name="text">The text to filter.</param>
        /// <returns>The filtered text.</returns>
        private static string Filter(string text)
        {
            string badWords = string.Empty;

            using (Stream stream = new FileStream(
                    Application.StartupPath + "\\Resources\\bad-words.txt", FileMode.Open))
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    badWords = reader.ReadToEnd();
                }
            }

            return Regex.Replace(text, badWords, "---", RegexOptions.IgnoreCase);  
        }
    }
}
